// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    /// <summary>
    ///     Represents the entry to the database, which contains the database connection information, 
    ///     methods for manipulating database transactions, and other easy-to-use properties.
    /// </summary>
    public abstract class DataContext : IDisposable
    {
        protected readonly IDataContextOptions _dataContextOptions;
        protected ISqlExecutor _sqlExecutor;
        protected ISqlModelMapper _modelMapper;

        /// <summary>
        ///     Initializes a new instance of the DataContext class by an IDataContextOptions object.
        /// </summary>
        /// <param name="dataContextOptions">An IDataConntextOptions object that contains options for the DataContext object. </param>
        protected DataContext(IDataContextOptions dataContextOptions)
        {
            _dataContextOptions = dataContextOptions;
            this.CurrentConnection = this.CreateAdoDbConnection(dataContextOptions.ConnectionString);

            this.CreateModel();
        }

        /// <summary>
        ///     Initializes a new instance of the DataContext class by a DbConnnection object and 
        ///     an IDataContextOptions object
        /// </summary>
        /// <param name="dbConnection">A DbConnection object which represents a connection to a database. </param>
        /// <param name="dataContextOptions">
        ///     An IDataConntextOptions object which contains options for the DataContext object. 
        ///     The ConnectionString option in this object is ignored and the dbConnection argument is used instead. 
        /// </param>
        protected DataContext(DbConnection dbConnection, IDataContextOptions dataContextOptions)
        {
            _dataContextOptions = dataContextOptions;
            this.CurrentConnection = this.CreateAdoDbConnection(dbConnection);

            this.CreateModel();
        }

        /// <summary>
        ///     Gets an IDataContextOptions object which contains options of the DataContext object
        /// </summary>
        public IDataContextOptions ContextOptions => _dataContextOptions;

        /// <summary>
        ///     Gets an ISqlExecutor object created by the current DataContext.
        /// </summary>
        public ISqlExecutor SqlExecutor
        {
            get
            {
                _sqlExecutor ??= new SqlExecutor(this);

                return _sqlExecutor;
            }
        }

        /// <summary>
        ///     Gets an ISqlModelMapper object created by the current DataContext.
        /// </summary>
        public ISqlModelMapper SqlModelMapper
        {
            get
            {
                _modelMapper ??= new SqlModelMapper(this);

                return _modelMapper;
            }
        }

        /// <summary>
        /// Gets an IAdoDbConnection object which can be used to get detailed information 
        /// about the database connection, as well as open or close the database connection
        /// </summary>
        public virtual IAdoDbConnection CurrentConnection { get; }

        /// <summary>
        /// Gets an IAdoDbTransaction object which can be used to get information about 
        /// the database transaction, as well as commit or roll back the transaction.
        /// </summary>
        public virtual IAdoDbTransaction CurrentTransaction => this.CurrentConnection.CurrentTransaction;

        /// <summary>
        /// Starts a database transaction, using the isolation level specified by the ContextOptions.
        /// </summary>
        /// <returns>Returns an IAdoDbTransaction object representing the new transaction.</returns>
        public virtual IAdoDbTransaction BeginTransaction()
        {
            return this.BeginTransaction(_dataContextOptions.IsolationLevel);
        }

        /// <summary>
        /// Starts a database transaction using the specified isolation level.
        /// </summary>
        /// <param name="isolationLevel">
        ///     An enumeration value for the IsolationLevel enumeration which specifies the transaction 
        ///     locking behavior for the connection.
        /// </param>
        /// <returns>Returns an IAdoDbTransaction object representing the new transaction.</returns>
        public virtual IAdoDbTransaction BeginTransaction(IsolationLevel isolationLevel)
        {
            return this.CurrentConnection.BeginTransaction(isolationLevel);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public virtual Task<IAdoDbTransaction> BeginTransactionAsync()
        {
            return this.BeginTransactionAsync(_dataContextOptions.IsolationLevel, default);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual Task<IAdoDbTransaction> BeginTransactionAsync(CancellationToken cancellationToken)
        {
            return this.BeginTransactionAsync(_dataContextOptions.IsolationLevel, cancellationToken);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="isolationLevel"></param>
        /// <returns></returns>
        public virtual Task<IAdoDbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel)
        {
            return this.BeginTransactionAsync(isolationLevel, default);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="isolationLevel"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual Task<IAdoDbTransaction> BeginTransactionAsync(
            IsolationLevel isolationLevel, CancellationToken cancellationToken)
        {
            return this.CurrentConnection.BeginTransactionAsync(isolationLevel, cancellationToken);
        }

        /// <summary>
        ///     Uses a transaction as the current database transaction.
        /// </summary>
        /// <param name="transaction">A DbTransaction object which will be used as the current database transaction.</param>
        /// <returns>Returns the new database transaction if succeeds; returns null If the transaction argument is null.</returns>
        public virtual IAdoDbTransaction UseTransaction(DbTransaction transaction)
        {
            return this.CurrentConnection.UseTransaction(transaction);
        }

        /// <summary>
        /// Commits the database transaction.
        /// </summary>
        public virtual void Commit()
        {
            if (this.CurrentTransaction != null)
            {
                this.CurrentTransaction.Commit();
            }
            else
            {
                throw new InvalidOperationException("Transaction is not started.");
            }
        }

        /// <summary>
        ///     Rolls back the current database transaction from a pending state.
        /// </summary>
        public virtual void Rollback()
        {
            if (this.CurrentTransaction != null)
            {
                this.CurrentTransaction.Rollback();
            }
            else
            {
                throw new InvalidOperationException("Transaction is not started.");
            }
        }

#if !NETSTANDARD2_0
        public virtual Task CommitAsync()
        {
            return this.CommitAsync(default);
        }

        public virtual Task CommitAsync(CancellationToken cancellationToken)
        {
            if (this.CurrentTransaction != null)
            {
                return this.CurrentTransaction.CommitAsync(cancellationToken);
            }
            else
            {
                throw new InvalidOperationException("Transaction is not started.");
            }
        }

        public virtual Task RollbackAsync()
        {
            return this.RollbackAsync(default);
        }

        public virtual Task RollbackAsync(CancellationToken cancellationToken)
        {
            if (this.CurrentTransaction != null)
            {
                return this.CurrentTransaction.RollbackAsync(cancellationToken);
            }
            else
            {
                throw new InvalidOperationException("Transaction is not started.");
            }
        }
#endif

        internal abstract IAdoDbProviderFactory AdoDbProviderFactory { get; }

        internal abstract IAdoDbSqlProvider AdoDbSqlProvider { get; }

        internal abstract IAdoDbSchemaProvider AdoDbSchemaProvider { get; }

        internal abstract IAdoDbConnection CreateAdoDbConnection(string connectionString);

        internal abstract IAdoDbConnection CreateAdoDbConnection(DbConnection dbConnection);

        /// <summary>
        ///     Releases the database transaction.
        /// </summary>
        public void Dispose()
        {
            if (this.CurrentTransaction != null)
            {
                this.CurrentTransaction.Rollback();

                throw new InvalidOperationException(
                    "Transaction is rollbacked. please call Commit()/Rollback() " +
                    "on DataContext after BeginTransaction().");
            }

            this.CurrentConnection?.Dispose();
        }

        protected virtual void OnModelCreating(ModelBuilder modelBuilder)
        {
        }

        private void CreateModel()
        {
            var modelBuilder = ModelBuilder.GetModelBuilder();

            this.OnModelCreating(modelBuilder);
        }
    }
}
